package com.starryx.websocket.chatroom.net.handler;

import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedNioFile;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.File;
import java.io.RandomAccessFile;
import java.net.URISyntaxException;
import java.net.URL;

/**
 * HTTP 请求处理器
 */
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    private static final Logger log = LogManager.getLogger(HttpRequestHandler.class);

    // 用于判断是否使用 websocket
    private final String wsUri;

    // HTML 文件对象
    private static final File INDEX;

    // 静态加载聊天室网页
    static {
        // 获取类加载路径（项目路径）
        URL location = HttpRequestHandler.class.getProtectionDomain().getCodeSource().getLocation();
        try {
            // 将项目路径和 HTML 文件名拼接成文件的路径
            String path = location.toURI() + "WebsocketChatClient.html";
            // 因为获取的路径可能包含有 file: 这种前缀，如果有则将其去掉，以免影响文件对象的创建
            path = !path.contains("file:") ? path : path.substring(5);
            // 创建 HTML 页面的对象
            INDEX = new File(path);
        } catch (URISyntaxException e) {
            throw new IllegalStateException("Unable to locate WebsocketChatClient.html", e);
        }
    }

    public HttpRequestHandler(String wsUri) {
        this.wsUri = wsUri;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        // 浏览器访问网页时会自动发送这个请求，用于请求网页小图标，在这里忽略即可
        if (request.uri().equals("/favicon.ico")) {
            return;
        }
        // 如果请求的路径是 wsUri（"/ws"）,则交由下一个 websocket 的处理器处理
        if (wsUri.equalsIgnoreCase(request.uri())) {
            // 如果请求是 WebSocket 升级，递增引用计数器（保留）并且将它传递给在 ChannelPipeline 中的下个 ChannelInboundHandler
            // 调用 retain 方法是让 request 变量能够传到下一个处理器，否则会在当前处理释放，因为 SimpleChannelInboundHandler 自带释放
            ctx.fireChannelRead(request.retain());
        } else {
            // 检查客户端发过来的请求是否带有 Expect: 100-continue
            if (HttpUtil.is100ContinueExpected(request)) {
                // 如果有，则进行处理
                send100Continue(ctx);
            }

            // 创建随机读取文件对象，这个对象允许直接跳转到文件任意位置进行读写操作，而不是从头到尾地读写
            // 主要方便下面分片传输
            RandomAccessFile file = new RandomAccessFile(INDEX, "r");

            // 创建一个响应对象，响应行为请求协议和 200 状态码
            HttpResponse response = new DefaultHttpResponse(request.protocolVersion(), HttpResponseStatus.OK);
            // 响应头设置数据类型和字符集编码
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");

            // 判断是否保持连接
            boolean keepAlive = HttpUtil.isKeepAlive(request);
            if (keepAlive) {
                // 响应头中设置数据内容长度
                response.headers().set(HttpHeaderNames.CONTENT_LENGTH, file.length());
                // 设置为 keepalive
                response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
            }
            ctx.write(response);

            // 判断 SslHandler 是否在 ChannelPipeline 来决定是使用 DefaultFileRegion 还是 ChunkedNioFile
            // 如果不需要加密和压缩，那么可以通过将 index.html 的内容存储到 DefaultFileRegion 中来达到最佳效率。这将会利用零拷贝特性来进行内容的传输。
            if (ctx.pipeline().get(SslHandler.class) == null) {
                ctx.write(new DefaultFileRegion(file.getChannel(), 0, file.length()));
            } else {
                ctx.write(new ChunkedNioFile(file.getChannel()));
            }
            // 最后发一个文件发送结束的标志，上面一个写操作都没有 flush ，直到这一步才进行 flush
            ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
            // 如果不保持连接，则关闭连接
            if (!keepAlive) {
                future.addListener(ChannelFutureListener.CLOSE);
            }
            // 最后关闭文件
            file.close();
        }
    }

    /**
     * 处理 100状态码期待请求
     * @param ctx
     */
    private static void send100Continue(ChannelHandlerContext ctx) {
        // 响应 100 状态码，表示接受客户端的发送请求，100 状态码常用于客户端发送大文件前，询问服务器是否愿意接收
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
        ctx.writeAndFlush(response);
    }

    /**
     * 异常捕捉处理
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        Channel curChannel = ctx.channel();
        log.error("Client： {} 异常", curChannel.remoteAddress());
        log.error(cause.getMessage());
        // 当出现异常就关闭连接
        ctx.close();
    }


}
